#!/usr/bin/python

# Copyright 2015 by Platform Product Integration Team.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, not be used in advertising or publicity
# pertaining to distribution

import os,sys,argparse,time,subprocess,json,re,pprint

__author__ = "Youye Sun <youye.sun@emc.com>"
__version__ = filter(str.isdigit, "$Revision: 3306 $")
__date__ = "5/27/2015"

__filename__ = os.path.basename(__file__)

LOG_FILE = __filename__[: __filename__.find('.') if __filename__.find('.') != -1 else len(__filename__) + 1] + \
              "_log_" + time.strftime("%m-%d-%Y-%H-%M-%S")

DEFAULT_TEST_CONFIG = "testcases.json"
DEFAULT_TESTBED = "testbeds.json"


def argument_parser():
    """
    Parse arguments if any
    """
    parser = argparse.ArgumentParser(prog=__filename__, usage='%(prog)s [options]')
    parser.add_argument('-tc', '--testconfig', help="provide configuration file for test cases", nargs='?',
                        const=DEFAULT_TEST_CONFIG, default=DEFAULT_TEST_CONFIG)
    parser.add_argument('-tbf', '--testbedfile', help="provide file containing names of test beds", nargs='+')
    parser.add_argument('-tb', '--testbed', help="names of test beds", nargs='+')
    parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
    parser.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true")
    parser.add_argument('-ds', '--detailedsummary', help="enable a detailed summary", action="store_true")
    return parser.parse_args()


args = argument_parser()


class Logger:
    def __init__(self, name):
        """
        Initialize a logger handler.

        Create a log file handler with name and timestamp.
        """
        self._handler = open(name, 'a+')

    def __del__(self):
        """
        Close the file handler when logger is being destroyed.
        """
        self._handler.close()

    def write(self, *lines):
        """
        Write lines to the log file and print the line on the console.
        """
        for line in lines:
            self._handler.write(line + '\n')
            print line

    """
    Define write function for verbose mode.
    If verbose mode is not turned on, verbose write will be None.
    """
    if args.verbose:
        def verbosewrite(self, *lines):
            self.write(*lines)
    else:
        verbosewrite = lambda *a: None


class Action:
    def __init__(self, cmd, retry_count, on_error):
        """
        Initialize an action.
        """
        self.cmd = cmd
        self.retry_count = retry_count
        self.on_error = on_error.upper()

    def toStr(self):
        """
        Get the string presentation of an action
        """
        return "cmd - " + self.cmd + ', retry count - ' + str(self.retry_count) + ', on error - ' + str(self.on_error)

    def run(self):
        """
        Run a command string.

        Run the command according to the retry count. Abort the program on error if specified.
        """
        p = subprocess.Popen(self.cmd, shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        i = 0
        while i < self.retry_count + 1:
            try:
                out, err = p.communicate()
                if p.returncode >= 0:
                    break
                i += 1
            except Exception, e:
                log.verbosewrite("EXCEPTION - run command" + self.toStr() + '\n' + e)
                p.kill()
        return p.returncode, out + err


class Check:
    def __init__(self, name, pattern, match_action, no_match_action):
        """
        Initialize an Check
        """
        self.name = name
        self.pattern = pattern
        self.match_action = match_action
        self.no_match_action = no_match_action

    def toStr(self):
        match_str = str(None) if self.match_action is None else self.match_action.toStr()
        no_match_str = str(None) if self.no_match_action is None else self.no_match_action.toStr()
        return "Pattern: " + self.pattern + "\nMatch Action: " + match_str + "\nNon Match Action: " + no_match_str

    def compare(self,string):
        """
        Compare the string with the pattern. Run the action according to the result of the comparison.
        """
        log.write(self.toStr())
        if string is None:
            log.write("String Being Compared Is None", "SKIPPING")
            return
        rc = None
        out = None
        if re.search(self.pattern.replace(' ', '').lower(), r""+string.replace(' ', '').lower()) is None:
            log.write("*** Test Result Does Not Match With Pattern ***")
            action = self.no_match_action
        else:
            log.write("*** Test Result Matches With Pattern ***")
            action = self.match_action
        if action is not None:
            log.verbosewrite("Run action: ", action.toStr())
            rc, out = action.run()
        return action, rc, out


class TestCase:
    def __init__(self, name, command_dict, checks_list):
        """
        Initialize a test case.

        Generate Action object and list of Check objects. The parameters are passed in as dictionary.
        """
        self.name = name
        self.command = self._generateActionObj(command_dict)
        self.checks = self._generateChecks(checks_list)

    def _generateActionObj(self, dict):
        """
        Generate Action object from dictionary containing command, retry count and on error information.
        """
        if dict is None or "cmd" not in dict.keys() or dict["cmd"] is None:
            return None
        cmd = dict["cmd"]
        retry_count = 0 if "retry_count" not in dict.keys() or dict["retry_count"] is None else dict["retry_count"]
        on_error_abort = "SKIP" if "on_error" not in dict.keys() or dict["on_error"] is None else dict["on_error"]
        return Action(cmd, retry_count, on_error_abort)

    def _generateCheckObj(self, name, dict):
        """
        Generate Check object from dictionary containing pattern and action information.
        """
        if dict is None or "pattern" not in dict.keys() or dict["pattern"] is None:
            return None
        pattern = dict["pattern"]
        match_dict = None if "match_action" not in dict.keys() else dict["match_action"]
        no_match_dict = None if "non_match_action" not in dict.keys() else dict["non_match_action"]
        return Check(name, pattern, self._generateActionObj(match_dict), self._generateActionObj(no_match_dict))

    def _generateChecks(self, list):
        """
        Generate a list of Check objects
        """
        if list is None or len(list) == 0:
            return None
        checks = []
        for l in list:
            for k, v in l.iteritems():
                checks.append(self._generateCheckObj(k, v))
        return checks

    def run(self):
        """
        Run the command of the test and compare the patterns with the result
        """
        log.write("Test >> " + self.name,"Total checks: " + str(len(self.checks)))
        if self.command is not None:
            log.write("Command: " + self.command.toStr())
            rc, output = self.command.run()
            if rc == 0:
                log.verbosewrite("Command result: \n" + output)
                for check in self.checks:
                    log.write("_-_" + "-"*25,
                              "Checking " + str(self.checks.index(check) + 1) + " out of " + str(len(self.checks)) + " checks",
                              "Check >> " + check.name)
                    ix = str(self.checks.index(check) + 1)
                    action, rcode, out = check.compare(output)
                    if rcode is not None and rcode != 0:
                        log.write("RUN ACTION FAILED -  check " + ix, "ERROR DETAIL: ", out)
                        if action.on_error == 'ABORT':
                            log.write("Skipping rest of the checks")
                            break
                        elif action.on_error == 'SKIP':
                            log.write("Continuing next check")
                            continue
                    elif rcode == 0:
                        log.write("Run action succeed -  check " + ix)
                    elif action == None:
                        log.verbosewrite("No action provided")
                if self.checks.index(check) < len(self.checks) - 1:
                    log.write("Checks that are not checked:")
                    for c in self.checks[self.checks.index(check) + 1:]:
                        log.write("Check >> " + c.name)
        else:
            log.write("Test " + self.name + " has no command to run")
            rc = None
            output =None
        return rc, output


class ArrayChecker():
    def __init__(self,testconfig):
        """
        Initialize a test engine.
        """
        self.testcases = []
        self._generateTestCases(testconfig)

    def _generateTestCases(self,testconfig):
        """
        Generate test cases from json file and append the test cases to the list of test cases.
        """
        try:
            with open(testconfig) as data_file:
                data = json.load(data_file)
                for d in data:
                    for k, v in d.iteritems():
                        cmd_dict = None if "command" not in v.keys() else v["command"]
                        checks_dict = None if "checks" not in v.keys() else v["checks"]
                        self.testcases.append(TestCase(k, cmd_dict, checks_dict))
                data_file.close()
        except Exception, e:
            log.write("Can not open configuration file")
            raise

    def run(self):
        """
        Run test cases.
        """
        total = str(len(self.testcases))
        log.write("Total test cases: " + total)
        for tc in self.testcases:
            ix = str(self.testcases.index(tc) + 1)
            log.write("_-_"+"="*50, "Running " + ix + " out of " + total + " test cases")
            rc, output = tc.run()
            if rc is not None and rc != 0:
                log.write("RUN COMMAND FAILED - test case " + ix + ": " + tc.command.toStr(), "ERROR DETAIL: ", output)
                log.write("Skipping all the checks:")
                for ch in tc.checks:
                    log.write("Check >> " + ch.name)
                if tc.command.on_error == 'ABORT':
                    log.write("Stopping array checker")
                    break
                elif tc.command.on_error == 'SKIP':
                    log.write("Continuing next test case")
                    continue
        if self.testcases.index(tc) < len(self.testcases) - 1:
            log.write("Test cases that are not run: ")
            for t in self.testcases[self.testcases.index(tc) + 1:]:
                log.write("_-_"+"-"*10,"Test >> " + t.name)
                for c in t.checks:
                    log.write("\tCheck >> " + c.name)


def _parseList(llist, delim):
    """
    Parse list by delim
    """
    ss = len(llist)
    start = 0
    res = list()
    for i in range(0, ss):
        if delim in llist[i]:
            res.append(llist[start:i])
            start = i + 1
    if start < ss:
        res.append(llist[start:ss])
    return res


def _fixStringLength(sstr, l):
    """
    Pad " " to sstr, if its length is less than l.
    Truncate sstr to length l, if its length is greater than l..
    """
    sstr = sstr.replace("\n", "")
    if len(sstr) < l:
        sstr += " " * (l - len(sstr))
    elif len(sstr) > l:
        sstr = sstr[0:l-2] + ".."
    return sstr


def _searchList(llist, depth, target):
    """
    Search lines in list for target.
    Return lines containing target.
    """
    res = list()
    if depth == 0:
        for l in llist:
            if target in l:
                res.append(l)
    else:
        for l in llist:
            res += _searchList(l, depth - 1, target)
    return res


def _getTestSketch(tc, failed_type):
    """
    Get details of a test case.
    type: 0 - PASS
          1 - RUN COMMAND FAILED
          2 - RUN ACTION FAILED
          3 - SKIPPED
    """
    res = dict()
    stmp = _searchList(tc, 0, "Test >> ")[0]
    res["test"] = _fixStringLength(stmp[stmp.find("Test >> ") + len("Test >> "):], 20)
    res["checks"] = list()
    res["pass"] = "Y"
    res["cmd"] = " "
    if failed_type == 1:
        res["pass"] = "N"
        res["cmd"] = "F"
    elif failed_type == 2:
        res["pass"] = "N"
        res["cmd"] = " "
        chs = _parseList(tc, "_-_"+"-"*25+"\n")[1:]
        for ch in chs:
            c = dict()
            stmp = _searchList(ch, 0, "Check >> ")[0]
            c["check"] = _fixStringLength(stmp[stmp.find("Check >> ") + len("Check >> "):], 20)
            if len(_searchList(ch, 0, "Test Result Matches With Pattern")) != 0:
                c["match"] = "Y"
            else:
                c["match"] = "N"
            if len(_searchList(ch, 0, "RUN ACTION FAILED")) == 0:
                c["action"] = " "
            else:
                c["action"] = "F"
            res["checks"].append(c)
        """
        check "checks" that are not run
        """
        notrun = _searchList(ch, 0, "Check >> ")
        if len(notrun) > 1:
            for nr in notrun[1:]:
                c = dict()
                c["check"] = _fixStringLength(nr[len("Check >> "):], 20)
                c["match"] = "SKP"
                c["action"] = " "
                res["checks"].append(c)
    elif failed_type == 3:
        res["pass"] = "SKP"
    return res


def _getFailedTestsSketch(tcs):
    """
    Get details of all the failed test cases.
    """
    res = list()
    failed = 0
    for t in tcs:
        tmp = _searchList(t, 0, "RUN COMMAND FAILED")
        if len(tmp) != 0:
            res.append(_getTestSketch(t, 1))
            failed += 1
        else:
            tmp = _searchList(t, 0, "RUN ACTION FAILED")
            if len(tmp) != 0:
                res.append(_getTestSketch(t, 2))
                failed += 1
            else:
                res.append(_getTestSketch(t, 0))
    """
    check test cases that are not run
    """
    notrun = _searchList(t, 0, "Test >>")
    if len(notrun) > 1:
        for nr in notrun[1:]:
            tmp = list()
            tmp.append(nr)
            res.append(_getTestSketch(tmp, 3))
    return res, failed


def _generateTestDetailText(tcs):
    text = list()
    text.append("-"*87)
    text.append("|" + "TEST" + " "*(25 - len("TEST")) +
                "|" + "PASS" + " "*(5 - len("PASS")) +
                "|" + "COMMAND" + " "*(10 - len("COMMAND")) +
                "|" + "CHECK" + " "*(25 - len("CHECK")) +
                "|" + "MATCH" + " "*(5 - len("MATCH")) +
                "|" + "ACTION" + " "*(10 - len("ACTION")) +
                "|")
    for tc in tcs:
        tx = "|"
        tx += tc["test"] + " "*(25 - len(tc["test"])) + "|"
        tx += tc["pass"] + " "*(5 - len(tc["pass"])) + "|"
        tx += tc["cmd"] + " "*(10 - len(tc["cmd"])) + "|"
        if len(tc["checks"]) == 0:
            tx += " "*25 + "|" + " "*5 + "|" + " "*10 + "|"
            text.append(tx)
        else:
            for ch in tc["checks"]:
                if tc["checks"].index(ch) != 0:
                    tx = "|" + " "*25 + "|" + " "*5 + "|" + " "*10 + "|"
                tx += ch["check"] + " "*(25 - len(ch["check"])) + "|"
                tx += ch["match"] + " "*(5 - len(ch["match"])) + "|"
                tx += ch["action"] + " "*(10 - len(ch["action"])) + "|"
                text.append(tx)
    text.append("-"*87)
    return text


def _getCheckDetail(ch):
    """
    Get details of a check in a test case
    """
    c = dict()
    stmp = _searchList(ch, 0, "Check >> ")[0]
    c["check"] = _fixStringLength(stmp[stmp.find("Check >> ") + len("Check >> "):], 20)
    if len(_searchList(ch, 0, "Test Result Matches With Pattern")) != 0:
        c["match"] = "Y"
    elif len(_searchList(ch, 0, "Test Result Does Not Match With Pattern")) != 0:
        c["match"] = "N"
    else:
        c["match"] = "SKP"
    if len(_searchList(ch, 0, "Run action succeed")) != 0:
        c["action"] = "S"
    elif len(_searchList(ch, 0, "RUN ACTION FAILED")) != 0:
        c["action"] = "F"
    elif len(_searchList(ch, 0, "No action provided")) != 0:
        c["action"] = "NP"
    else:
        c["action"] = "SKP"
    return c


def _onedToTwodList(llist):
    res = list()
    for l in llist:
        ll = list()
        ll.append(l)
        res.append(ll)
    return res


def _getTestDetail(tc, tc_type):
    """
    Get details of a test case.
    type: 0 - PASS
          1 - RUN COMMAND FAILED
          2 - RUN ACTION FAILED
          3 - SKIPPED
    """
    res = dict()
    stmp = _searchList(tc, 0, "Test >> ")[0]
    res["test"] = _fixStringLength(stmp[stmp.find("Test >> ") + len("Test >> "):], 20)
    res["checks"] = list()
    res["pass"] = " "
    res["cmd"] = " "
    if tc_type == 0:
        res["pass"] = "Y"
        res["cmd"] = "S"
        chs = _parseList(tc, "_-_"+"-"*25+"\n")[1:]
    elif tc_type == 1:
        res["pass"] = "N"
        res["cmd"] = "F"
        chs = _parseList(tc, "_-_"+"-"*10+"\n")[0]
        chsl = _searchList(chs, 0, "Check >> ")
        chs = _onedToTwodList(chsl)
    elif tc_type == 2:
        res["pass"] = "N"
        res["cmd"] = "S"
        chs = _parseList(tc, "_-_"+"-"*25+"\n")[1:]
        if len(_searchList(chs[-1], 0, "Check >> ")) > 1:
            chs = chs + _onedToTwodList(_searchList(chs[-1], 0, "Check >> ")[1:])
    elif tc_type == 3:
        res["pass"] = "SKP"
        res["cmd"] = "SKP"
        chs = _onedToTwodList(_searchList(tc, 0, "\tCheck >> "))
    for ch in chs:
        res["checks"].append(_getCheckDetail(ch))
    return res


def _getAllTestsDetail(tcs):
    """
    Get details of all test cases.
    """
    res = list()
    for t in tcs:
        tp = _searchList(t, 0, "RUN COMMAND FAILED")
        if len(tp) != 0:
            res.append(_getTestDetail(t, 1))
        else:
            tp = _searchList(t, 0, "RUN ACTION FAILED")
            if len(tp) != 0:
                res.append(_getTestDetail(t, 2))
            else:
                res.append(_getTestDetail(t, 0))
    """
    show test cases that are not run
    """
    #pprint.pprint(t)
    notrun = _parseList(t, "_-_"+"-"*10 +'\n')
    #pprint.pprint(notrun)
    #print "number of not runs" + str(len(notrun))
    if len(notrun) > 1:
        for nr in notrun[1:]:
            res.append(_getTestDetail(nr, 3))
    return res


def logSummery(file_path):
        """
        Get a summery of the run from log file
        """
        summ = dict()
        with open(file_path, "r") as logsum:
            lines = logsum.readlines()
            tcs = _parseList(lines, "_-_"+"="*50+"\n")
            res = _searchList(tcs[0], 0, "Total test cases")
            summ["total"] = res[0].replace("\n", "")
            summ["total runs"] = "Total test cases attempted: " + str(len(tcs) - 1)
            summ["testbed"] = _searchList(tcs[0], 0, "Test Bed - ")[0].replace("\n", "")
            res, failed = _getFailedTestsSketch(tcs[1:])
            text = _generateTestDetailText(res)
            if args.detailedsummary:
                all_detail = _getAllTestsDetail(tcs[1:])
                detail_text = _generateTestDetailText(all_detail)
            logsum.close()
        logg = Logger(file_path)
        logg.write("\n"*3, "*"*5 + " Summery " + "*"*5, summ["testbed"], summ["total"], summ["total runs"],
                   "Number of failed runs: " + str(failed), "Sketch: Y-yes, N-no, SKP-skipped, F-failed")
        for t in text:
            logg.write(t)
        if args.detailedsummary:
            logg.write("Details: Y-yes, N-no, SKP-skipped, S-succeed, F-failed, NP-not provided")
            for t in detail_text:
                logg.write(t)
        del logg


def _getTestBed():
    """
    Get names of test beds from arguments
    """
    res = list()
    if args.testbed:
        res += args.testbed
    if args.testbedfile:
        for fl in args.testbedfile:
            with open(fl, "r") as file_data:
                lines = file_data.readlines()
                for l in lines:
                    if l != '\n':
                        res.append(l.replace('\n', ''))
    return res


def _fileSearchReplace(ffile, target, replace):
    """
    Replace 'target' in file with 'replace'.
    Return name of the file containing replacement.
    """
    tmp = replace + '-' + ffile
    with open(tmp, "w+") as new_file:
        with open(ffile) as file_data:
            lines = file_data.readlines()
            for l in lines:
                new_file.write(l.replace(target, replace))
    return tmp


if __name__ == '__main__':
    testbeds = _getTestBed()
    if len(testbeds) == 0:
        print "No test bed provided."
        sys.exit()
    for tb in testbeds:
        log_file = tb + '-' + LOG_FILE
        global log
        log = Logger(log_file)
        print "Log File: " + log_file
        log.write("Test Bed - " + tb)
        if args.testconfig != DEFAULT_TEST_CONFIG:
            log.write("The configuration file for test cases: " + args.testconfig)
        else:
            log.write("Use default test configuration file: " + DEFAULT_TEST_CONFIG)
        tmp = _fileSearchReplace(args.testconfig, '<array>', tb)
        arraychecker = ArrayChecker(tmp)
        arraychecker.run()
        del log
        logSummery(log_file)
        os.remove(tmp)


